<?php
require_once 'lib/share/Snoopy.class.php';

/** 
 * 酷武公司接口[简称kuwu]API。
 * @author alex  
 * @version v1.0  2010-08-14
 */

function toDotNETDSS1Signation($signature){
  $pos=4;
  $rcount=ord($signature[$pos-1]);

  if($rcount==20){
	$ans1r=substr($signature,$pos,20);
	$pos+=20;
  } 
  else if($rcount>20){ 
	$ans1r=substr($signature,$pos+1,20);
	$pos+=21;
  }
  else{
    $ans1r=substr($signature,$pos,$rcount);
    for($i=0;$i<20-$rcount;$i++) $ans1r = chr(0) . $ans1r;
    $pos+=$rcount; 	
  }  

  $pos+=2;
  $rcount=ord($signature[$pos-1]);

  if($rcount==20)	$ans1s=substr($signature,$pos,20);
  else if($rcount>20)	$ans1s=substr($signature,$pos+1,20);
  else{
  	$ans1s=substr($signature,$pos,$rcount);
  	for($i=0;$i<20-$rcount;$i++) $ans1s = chr(0) . $ans1s;
  }  
  return  $ans1r . $ans1s;	
}

function fromDotNETDSS1Signation($signature){
	$ans1r=substr($signature,0,20);
	$ans1s=substr($signature,20,20);
	$head=chr(hexdec('30')).chr(hexdec('2C')); 
	$sep=chr(hexdec('02')).chr(hexdec('14'));
	return $head.$sep.$ans1r.$sep.$ans1s;
}

class KuWuRequest {
	/**
	 * @var string $host_url API的网站地址
	 */	
	var $host_url='http://www.smtkw.com/';
	
	/**
	 * @var array $app_token 应用app访问API的令牌，参数包括：<br/>
	 * string app_myprivatekey : 必须， 我方的私钥.<br/>
	 * string app_kuwupublickey :必须， 酷武公司的公钥<br/>
	 * string app_myprivatekey_pwd   :可选， 我方私钥的密码，默认:NULL。
	 * boolean app_key_is_file : 可选， key是否是文件，默认:false。
	 */
	var $app_token=array();
	
	/**
	 * @var array $app_setting 应用app访问API的通用参数设置，一般不改变默认值，包括：<br/>
	 * strng  sign_method :签名方法，dss1。<br/>
	 * string v：API接口版本号，v1.0<br/>
	 */
	var $app_setting=array('sign_method'=>OPENSSL_ALGO_DSS1, 
	                       'v'=>'v1.0');
	private $privkey=NULL;
	private $pubkey=NULL;
	/**
	 * 类构造函数
	 * @param array $app_token 应用app访问API的令牌，参见@see $app_token。
	 * 如果不传，签名、验证数字签名时总是动态生成key resource；如果传入令牌，将预先生成key resource，签名、验证数字签名速度较快，
	 * 如果需要改变访问API的令牌，改变属性@see app_token的值后，再调用函数@see refreshKey刷新key resource。
	 */
	function __construct($app_token=NULL){
	  if(isset($app_token) && is_array($app_token)){
	     $this->app_token=$app_token;
	     $this->refreshKey();
		}  		
	}
	function __destruct(){
		if($this->privkey) openssl_free_key($this->privkey);
		if($this->pubkey) openssl_free_key($this->pubkey);
	}
	function refreshKey(){
	     $keyisfile=$this->app_token['app_key_is_file'] ? true : false;
	     $pwd=$this->app_token['app_myprivatekey_pwd'] ? $this->app_token['app_myprivatekey_pwd'] : NULL;
	     $privkey=$this->app_token['app_myprivatekey'];
         if($keyisfile){
        	if(file_exists($privkey))
        	  $privkey=file_get_contents($privkey);
        	else
        	  throw new Exception('找不到私钥文件,' . $privkey);
          }
          
          if($this->privkey) openssl_free_key($this->privkey);
          if(isset($pwd))
            $this->privkey=openssl_pkey_get_private($privkey,$pwd);
          else 
            $this->privkey=openssl_pkey_get_private($privkey);
          if(! $this->privkey)
            throw new Exception('得不到私钥 ,' . openssl_error_string()); 
          
		  $pubkey=$this->app_token['app_kuwupublickey'];
          if($keyisfile){
          	if(file_exists($pubkey))
          	  $pubkey=file_get_contents($pubkey);
        	else
        	  throw new Exception('找不到公钥文件,' . $pubkey);
          }
          
          if($this->pubkey) openssl_free_key($this->pubkey);
          $this->pubkey=openssl_pkey_get_public($pubkey);
          if(! $this->pubkey)
            throw new Exception('得不到公钥 ,' . openssl_error_string()); 
	}
	/**
	 * 接受API的返回字符串。
	 * @param array $kuwu_req_array 通过$_REQUEST得到的参数数组。
	 * @return array|bool 调用的api接口得到的字符串。
	 */	    
    function recv(array $kuwu_req_array){
 		if(! $this->app_token['app_kuwupublickey'] || ! $this->app_setting['sign_method']) 
		   return false; 
		   
		
		$kuwu_xml=$kuwu_req_array['LOGISTICS_INTERFACE'];
		$kuwu_digest=$kuwu_req_array['DATA_DIGEST'];
		$kuwu_digest=fromDotNETDSS1Signation(base64_decode($kuwu_digest));
		if($this->pubkey){
			 $result=openssl_verify($kuwu_xml,$kuwu_digest,$this->pubkey,$this->app_setting['sign_method']);
             if($result===-1)  throw new Exception('验证失败 ,' . openssl_error_string());	
            $verifyresult=$result>0;	 
		}
		else{
		  $keyisfile=$this->app_token['app_key_is_file'] ? true : false;
		  $verifyresult=self::verify($kuwu_xml,$kuwu_digest,$this->app_token['app_kuwupublickey'],$keyisfile,$this->app_setting['sign_method']);
		}
		if(!$verifyresult)  throw new Exception('签名验证失败 ,' . openssl_error_string());	
		return iconv('GBK','UTF-8',$kuwu_xml);
    }
    
	/**
	 * 得到API的返回字符串。
	 * @param string $kuwu_xml 要调用的api接口的参数xml，一般通用的参数不要传。
	 * @return string|bool 调用的api接口得到的字符串。
	 */
	function get($kuwu_xml){
		if(! $this->app_token['app_myprivatekey'] || ! $this->app_setting['sign_method']) 
		   return false;
		
		$kuwu_xml=iconv('UTF-8','GBK//IGNORE',$kuwu_xml);
		$kuwu_type=iconv('UTF-8','GBK',$this->app_setting['v']);
		
		//sign xml
		if($this->privkey){
            $result=openssl_sign($kuwu_xml,$signdata,$this->privkey,$this->app_setting['sign_method']);
           if(!$result)  throw new Exception('签名失败 ,' . openssl_error_string());
		}else{
		  $pwd=$this->app_token['app_myprivatekey_pwd'] ? $this->app_token['app_myprivatekey_pwd'] : NULL;
		  $keyisfile=$this->app_token['app_key_is_file'] ? true : false;
		  $signdata=self::sign($kuwu_xml,$this->app_token['app_myprivatekey'],$keyisfile,$this->app_setting['sign_method'],$pwd);
		}
		$kuwu_digest=base64_encode(toDotNETDSS1Signation($signdata));
		//prepare param
		$host_url = $this->host_url;
		$kuwu_param=array('logistics_interface'=>$kuwu_xml,'data_digest'=>$kuwu_digest,'type'=>$kuwu_type);
		
		//call  method
        $snoopy = new Snoopy();
        $snoopy->accept='text/xml';
        $snoopy->submit($host_url,$kuwu_param); 
        
        if($snoopy->error)  throw new Exception($snoopy->error);
        return iconv('GBK','UTF-8//IGNORE',$snoopy->results);
	}
	
	/**
	 * 得到API的返回值，xml生成的数组。
	 * @param array $paramArr 要调用的api接口的参数数组。
	 * @return array|bool 调用的api接口得到的数组。
	 */	
	function getArray(array $paramArr){
		    $kuwu_xml=self::encodeXmlString($paramArr);
			$result=$this->get($kuwu_xml);
			$arr=array(); 
			if(! self::parseXmlString($result,$arr)) return false;
			return $arr;
	}	
	/**
	 * 接受API的返回值，xml生成的数组。
	 * @param array $kuwu_req_array 通过$_REQUEST得到的参数数组。
	 * @return array|bool 调用的api接口得到的数组。
	 */	
	function recvArray(array $kuwu_req_array){
			$result=$this->recv($kuwu_req_array);
			$arr=array(); 
			if(! self::parseXmlString($result,$arr)) return false;
			return $arr;
	}	
	/**
	 * 检查接口调用get*函数返回是否有错误
	 * @param  mixed $result  接口调用get*函数返回值.
	 * @return bool true 接口调用有效，false 接口调用出错.
	 */
	var $result_error_string=array(
     'S01'=>'非法的XML格式',
     'S02'=>'非法的数字签名',
     'S03'=>'非法的仓储公司',
     'S04'=>'非法的通知类型',
     'S05'=>'非法的通知状态',
     'S07'=>'系统异常请重试',
     'S08'=>'非法的仓库编号',
     'S09'=>'非法的订单编号',
     'S10'=>'非法的物流公司',
     'S11'=>'非法的商户编号',
     'S12'=>'非法的产品编号'	
	);
	function checkResult($result){
		if(! array_key_exists('Response',$result))
		  throw new Exception('标准返回格式错误，找不到Response');
		$reponse=$result['Response'];
		if($reponse['success']==='true') return true;
		$reason=$reponse['reason'];
		if($reason)  throw new Exception('API调用失败，'.$this->result_error_string[$reason] .'['.$reason.']');
		else throw new Exception('API调用失败');
	}


	/*
	 * 签名函数
	 */
    static function sign($data,$privkey,$keyisfile=false,$method=OPENSSL_ALGO_SHA1,$pwd=NULL) {
        if($keyisfile){
        	if(file_exists($privkey))
        	  $privkey=file_get_contents($privkey);
        	else
        	  throw new Exception('找不到私钥文件,' . $privkey);
        }
        if(isset($pwd))
          $key=openssl_pkey_get_private($privkey,$pwd);
        else 
          $key=openssl_pkey_get_private($privkey);
        if(! $key)
          throw new Exception('得不到私钥 ,' . openssl_error_string()); 
          
        $result=openssl_sign($data,$sign,$key,$method);
        openssl_free_key($key);
        if(!$result)
          throw new Exception('签名失败 ,' . openssl_error_string());
		return $sign;
	}
	
    static function verify($data,$sign,$pubkey,$keyisfile=false,$method=OPENSSL_ALGO_SHA1) {
        if($keyisfile){
        	if(file_exists($pubkey))
        	  $pubkey=file_get_contents($pubkey);
        	else
        	  throw new Exception('找不到公钥文件,' . $pubkey);
        }
        $key=openssl_pkey_get_public($pubkey);
        if(! $key)
          throw new Exception('得不到公钥 ,' . openssl_error_string()); 
        $result=openssl_verify($data,$sign,$key,$method);
        openssl_free_key($key);
        if($result===-1)
          throw new Exception('验证失败 ,' . openssl_error_string());
		return $result>0;
	}	
	static function encodeXmlString(array $data){
        reset($data);
        list($key,$val)=each($data);
        $valstr='';
        self::__pumpArray($val,$valstr,$key);
        $key=trim($key);
		return '<'.$key.'>' . $valstr . '</' . $key . '>';
	}
	static function __pumpArray(array &$dataarr,&$resultstr,$parentKey=NULL){
		$key_is_int=false;
		foreach($dataarr as $key=>$val){
			if(is_array($val) && count($val)>0){
				$valstr=''; 
				$now_key_is_int=self::__pumpArray($val,$valstr,$key);
				if(is_int($key)){
				  $key_is_int=true;
				  $resultstr .='<'.$parentKey.'>'.urlencode($valstr).'</'.$parentKey.'>';
				}
				else if(! $now_key_is_int){
				  $key=trim($key);
 			      $resultstr .='<'.$key.'>'.urlencode($valstr).'</'.$key.'>';
				}
 			    else {
 			     $resultstr .= urlencode($valstr);
 			    }
				 
			}
			else{
				$key=trim($key);
				$val=htmlspecialchars($val);
				$resultstr .='<'.$key.'>'.urlencode($val).'</'.$key.'>';
			}  
		}
		return $key_is_int;		
	}
	/**
	 * xml字符串解析为数组，xml字符串如<orders><order orderid="97913707" merchantid="6619" 
	 * paysum="32.90" isdeleted="0" ispaid="0" status="1" merchantorderid="" /></orders>
	 * <br/>如果不是xml字符串，不解析。<br/>原如果解析失败，抛出Execption异常
	 * @param string $xmlString xml字符串
	 * @param array $resultarr 解析后的结果数组
	 */
	static function parseXmlString(&$xmlString,array &$arr){
		$xmlString=trim($xmlString);
		if(strpos($xmlString,'<')!==0) return false; //simple test is xml
		try{
		  $root =new SimpleXMLElement($xmlString);
		  $name=$root->getName();
		  $resultarr=array();
		  self::__fillArray($root,$resultarr);
		  $arr[$name]=$resultarr; //set return val form
		  return true;
		}catch (Exception $e){
			throw new Exception('返回xml解析错误:' . $e->getMessage());
		}
	}	
	//----private function ------//
	static function __fillArray($root,array &$resultarr){
		if(! $root) return;
		$rootarr=get_object_vars($root);
		foreach ($rootarr as $key=>$val) {
			 if(is_object($val)){
				$newarr=array();
				self::__fillArray($val,$newarr);
			 	if(count($newarr) >0 )  $resultarr[$key]=$newarr;
			 	else  $resultarr[$key]=NULL;
			}
			else if(is_array($val) && count($val)>0){//is array
				$valarr=array();
				for($i=0;$i<count($val);$i++){
					$newarr=array();
					self::__fillArray($val[$i],$newarr);
					$valarr[]=$newarr;
				}
				$resultarr[$key]=$valarr;
			}
			else
			 $resultarr[$key]=$val;
		}		
	}
	
}

